using LightCAD.MathLib;
using System;
using System.Collections;
using System.Collections.Generic;


namespace LightCAD.Three
{
    public class PolyhedronGeometry : BufferGeometry
    {
        public class Parameters
        {
            public ListEx<double> vertices;
            public ListEx<int> indices;
            public double radius;
            public double detail;
        }
        #region Properties

        public Parameters parameters;

        #endregion

        #region constructor
        public PolyhedronGeometry(ListEx<double> vertices = null, ListEx<int> indices = null, double radius = 1, double detail = 0)
        {
            this.type = "PolyhedronGeometry";
            this.parameters = new Parameters()
            {
                vertices = vertices,
                indices = indices,
                radius = radius,
                detail = detail
            };
            // default buffer data
            var vertexBuffer = new ListEx<double>();
            var uvBuffer = new ListEx<double>();
            // the subdivision creates the vertex buffer data
            subdivide(detail);
            // all vertices should lie on a conceptual sphere with a given radius
            applyRadius(radius);
            // finally, create the uv data
            generateUVs();
            // build non-indexed geometry
            this.setAttribute("position", new Float32BufferAttribute(vertexBuffer.ToArray(), 3));
            this.setAttribute("normal", new Float32BufferAttribute(vertexBuffer.Slice().ToArray(), 3));
            this.setAttribute("uv", new Float32BufferAttribute(uvBuffer.ToArray(), 2));
            if (detail == 0)
            {
                this.computeVertexNormals(); // flat normals
            }
            else
            {
                this.normalizeNormals(); // smooth normals
            }
            // helper functions
            void subdivide(double _detail)
            {
                var a = new Vector3();
                var b = new Vector3();
                var c = new Vector3();
                // iterate over all faces and apply a subdivision with the given detail value
                for (int i = 0; i < indices.Length; i += 3)
                {
                    // get the vertices of the face
                    getVertexByIndex(indices[i + 0], a);
                    getVertexByIndex(indices[i + 1], b);
                    getVertexByIndex(indices[i + 2], c);
                    // perform subdivision
                    subdivideFace(a, b, c, _detail);
                }
            }
            void subdivideFace(Vector3 a, Vector3 b, Vector3 c, double _detail)
            {
                var cols = _detail + 1;
                // we use this multidimensional array as a data structure for creating the subdivision
                var v = new ListEx<ListEx<Vector3>>();
                // construct all of the vertices for this subdivision
                for (int i = 0; i <= cols; i++)
                {
                    v.Push(new ListEx<Vector3>());
                    var aj = a.Clone().Lerp(c, i / cols);
                    var bj = b.Clone().Lerp(c, i / cols);
                    var rows = cols - i;
                    for (int j = 0; j <= rows; j++)
                    {
                        if (j == 0 && i == cols)
                        {
                            v[i].Push(aj);
                        }
                        else
                        {
                            v[i].Push(aj.Clone().Lerp(bj, j / rows));
                        }
                    }
                }
                // construct all of the faces
                for (int i = 0; i < cols; i++)
                {
                    for (int j = 0; j < 2 * (cols - i) - 1; j++)
                    {
                        var k = (int)Math.Floor(j / 2.0);
                        if (j % 2 == 0)
                        {
                            pushVertex(v[i][k + 1]);
                            pushVertex(v[i + 1][k]);
                            pushVertex(v[i][k]);
                        }
                        else
                        {
                            pushVertex(v[i][k + 1]);
                            pushVertex(v[i + 1][k + 1]);
                            pushVertex(v[i + 1][k]);
                        }
                    }
                }
            }
            void applyRadius(double _radius)
            {
                var vertex = new Vector3();
                // iterate over the entire buffer and apply the radius to each vertex
                for (int i = 0; i < vertexBuffer.Length; i += 3)
                {
                    vertex.X = vertexBuffer[i + 0];
                    vertex.Y = vertexBuffer[i + 1];
                    vertex.Z = vertexBuffer[i + 2];
                    vertex.Normalize().MulScalar(radius);
                    vertexBuffer[i + 0] = vertex.X;
                    vertexBuffer[i + 1] = vertex.Y;
                    vertexBuffer[i + 2] = vertex.Z;
                }
            }
            void generateUVs()
            {
                var vertex = new Vector3();
                for (int i = 0; i < vertexBuffer.Length; i += 3)
                {
                    vertex.X = vertexBuffer[i + 0];
                    vertex.Y = vertexBuffer[i + 1];
                    vertex.Z = vertexBuffer[i + 2];
                    var u = azimuth(vertex) / 2 / MathEx.PI + 0.5;
                    var v = inclination(vertex) / MathEx.PI + 0.5;
                    uvBuffer.Push(u, 1 - v);
                }
                correctUVs();
                correctSeam();
            }
            void correctSeam()
            {
                // handle case when face straddles the seam, see #3269
                for (int i = 0; i < uvBuffer.Length; i += 6)
                {
                    // uv data of a single face
                    var x0 = uvBuffer[i + 0];
                    var x1 = uvBuffer[i + 2];
                    var x2 = uvBuffer[i + 4];
                    var max = MathEx.Max(x0, x1, x2);
                    var min = MathEx.Min(x0, x1, x2);
                    // 0.9 is somewhat arbitrary
                    if (max > 0.9 && min < 0.1)
                    {
                        if (x0 < 0.2) uvBuffer[i + 0] += 1;
                        if (x1 < 0.2) uvBuffer[i + 2] += 1;
                        if (x2 < 0.2) uvBuffer[i + 4] += 1;
                    }
                }
            }
            void pushVertex(Vector3 vertex)
            {
                vertexBuffer.Push(vertex.X, vertex.Y, vertex.Z);
            }
            void getVertexByIndex(int index, Vector3 vertex)
            {
                var stride = index * 3;
                vertex.X = vertices[stride + 0];
                vertex.Y = vertices[stride + 1];
                vertex.Z = vertices[stride + 2];
            }
            void correctUVs()
            {
                var a = new Vector3();
                var b = new Vector3();
                var c = new Vector3();
                var centroid = new Vector3();
                var uvA = new Vector2();
                var uvB = new Vector2();
                var uvC = new Vector2();
                for (int i = 0, j = 0; i < vertexBuffer.Length; i += 9, j += 6)
                {
                    a.Set(vertexBuffer[i + 0], vertexBuffer[i + 1], vertexBuffer[i + 2]);
                    b.Set(vertexBuffer[i + 3], vertexBuffer[i + 4], vertexBuffer[i + 5]);
                    c.Set(vertexBuffer[i + 6], vertexBuffer[i + 7], vertexBuffer[i + 8]);
                    uvA.Set(uvBuffer[j + 0], uvBuffer[j + 1]);
                    uvB.Set(uvBuffer[j + 2], uvBuffer[j + 3]);
                    uvC.Set(uvBuffer[j + 4], uvBuffer[j + 5]);
                    centroid.Copy(a).Add(b).Add(c).DivScalar(3);
                    var azi = azimuth(centroid);
                    correctUV(uvA, j + 0, a, azi);
                    correctUV(uvB, j + 2, b, azi);
                    correctUV(uvC, j + 4, c, azi);
                }
            }
            void correctUV(Vector2 uv, int stride, Vector3 vector, double _azimuth)
            {
                if ((_azimuth < 0) && (uv.X == 1))
                {
                    uvBuffer[stride] = uv.X - 1;
                }
                if ((vector.X == 0) && (vector.Z == 0))
                {
                    uvBuffer[stride] = _azimuth / 2 / MathEx.PI + 0.5;
                }
            }
            // Angle around the Y axis, counter-clockwise when looking from above.
            double azimuth(Vector3 vector)
            {
                return Math.Atan2(vector.Z, -vector.X);
            }
            // Angle above the XZ plane.
            double inclination(Vector3 vector)
            {
                return Math.Atan2(-vector.Y, Math.Sqrt((vector.X * vector.X) + (vector.Z * vector.Z)));
            }
        }
        #endregion


        #region methods
        public PolyhedronGeometry copy(PolyhedronGeometry source)
        {
            base.copy(source);
            this.parameters = new Parameters()
            {
                vertices = source.parameters.vertices,
                indices = source.parameters.indices,
                radius = source.parameters.radius,
                detail = source.parameters.detail,
            };
            return this;
        }
        #endregion
    }
}
